package com.hockeo.gwt;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;
import com.hockeo.client.metadata.IMetadataTag;
import com.hockeo.client.metadata.MetadataFactory;

/**
 * Generates the code for the {@link MetadataFactory} when it appears in a GWT.create()
 * expression.
 *
 * @version $Id$
 * @author jjanke
 */
public class MetadataGenerator extends Generator
{
  private TypeOracle d_oracle;
  private JClassType d_typeMetadataTag;

  @Override
  public String generate( TreeLogger logger, GeneratorContext ctx, String strTypeName )
      throws UnableToCompleteException
  {
    d_oracle = ctx.getTypeOracle();
    d_typeMetadataTag = d_oracle.findType( IMetadataTag.class.getName() );

    final List<JClassType> listTypes = new ArrayList<JClassType>();
    final JClassType[] atypes = d_oracle.getTypes();

    // search all classes implementing the IMetadataTag interface
    //
    for ( JClassType type : atypes )
      if ( isMetadataTagged( type ) )
        listTypes.add( type );

    final String strGenPackageName = MetadataFactory.class.getPackage().getName();
    final String strGenClassName = MetadataFactory.class.getSimpleName() + "Impl";
    final ClassSourceFileComposerFactory composer = new ClassSourceFileComposerFactory( strGenPackageName,
        strGenClassName );

    composer.setSuperclass( MetadataFactory.class.getSimpleName() );
    composer.addImport( Map.class.getName() );
    composer.addImport( HashMap.class.getName() );

    PrintWriter pw = ctx.tryCreate( logger, strGenPackageName, strGenClassName );

    // If pw is null, no work needs to be done (the class has already been generated
    // before)
    //
    if ( pw != null )
    {
      SourceWriter sw = composer.createSourceWriter( ctx, pw );

      sw.println( "protected Map<Class<?>, Metadata<?>> createMetadata()" );
      sw.println( "{" );
      sw.indent();
      sw.println( "Map<Class<?>, Metadata<?>> map = new HashMap<Class<?>, Metadata<?>>();" );
      sw.println( "Metadata md;" );
      sw.println();

      // create metadata objects for all required types
      //
      for ( JClassType type : listTypes )
      {
        generateMetadata( logger, sw, type );
        sw.println();
      }

      sw.println( "return map;" );
      sw.outdent();
      sw.println( "}" );

      sw.commit( logger );
    }

    return composer.getCreatedClassName();
  }

  private void generateMetadata( TreeLogger logger, SourceWriter sw, JClassType type )
  {
    List<JMethod> listGetters = findGetters( type );

    TreeLogger logMetadata = logger.branch( TreeLogger.INFO, "Generate metadata for " + type.getQualifiedSourceName()
        + "..." );

    sw.println( "PropertyDescriptor pd;" );
    sw.println();

    sw.println( "md = new Metadata(" + type.getQualifiedSourceName() + ".class);" );
    sw.println();

    for ( JMethod m : listGetters )
      generatePropertyDescriptor( logMetadata, sw, m );

    sw.println( "map.put(" + type.getQualifiedSourceName() + ".class, md);" );
  }

  private void generatePropertyDescriptor( TreeLogger logger, SourceWriter sw, JMethod method )
  {
    // strip off "get" or "is" prefix and lower first letter to get the property name
    //
    String strMethodName = method.getName();
    String strPropName = lowerFirst( strMethodName.substring( strMethodName.startsWith( "g" ) ? 3 : 2 ) );
    String strType;

    if ( method.getReturnType() instanceof JPrimitiveType )
      strType = ( (JPrimitiveType) method.getReturnType() ).getQualifiedBoxedSourceName();
    else
      strType = method.getReturnType().getQualifiedSourceName();

    logger.log( TreeLogger.INFO, "Found property: " + strPropName + " (" + strType + ")" );

    sw.println( "pd = md.addProperty(\"" + strPropName + "\"," + strType + ".class);" );
  }

  /**
   * Finds all the getter methods of the given class type.
   *
   * @param cls the class type from which to retrieve getter methods
   * @return the list of methods being recognized as getters
   */
  private List<JMethod> findGetters( JClassType cls )
  {
    List<JMethod> listMethods = new ArrayList<JMethod>();

    addGetters( cls, listMethods );

    return listMethods;
  }

  /**
   * Adds all the getter methods found in the given type to the specified list. The
   * getters from all super classes with the exception of Object are also added.
   *
   * @param cls the class from which to extract the getter methods
   * @param listMethods the list to which to add all relevant getter methods
   */
  private void addGetters( JClassType cls, List<JMethod> listMethods )
  {
    // ignore methods of Object
    //
    if ( cls.getSuperclass() == null )
      return;

    addGetters( cls.getSuperclass(), listMethods );
    for ( JMethod m : cls.getMethods() )
    {
      if ( m.isPublic() )
      {
        String strName = m.getName();

        if ( ( strName.matches( "get.*" ) || strName.matches( "is.*" ) ) && m.getParameters().length == 0 )
          listMethods.add( m );
      }
    }
  }

  /**
   * Lowers the first letter of the given string considered to be a property name.
   *
   * @param strPropName the property name for which to lower the first letter
   * @return the input string with the first letter in lower case
   */
  private String lowerFirst( String strPropName )
  {
    if ( strPropName.length() == 0 )
    {
      return strPropName;
    }
    else if ( strPropName.length() == 1 )
    {
      return strPropName.toLowerCase();
    }
    else
    {
      return strPropName.substring( 0, 1 ).toLowerCase() + strPropName.substring( 1 );
    }
  }

  /**
   * Checks if the given type is tagged with the {@link IMetadataTag} interface.
   *
   * @param type the type to be checked
   * @return <code>true</code> if the type is assignable to {@link IMetadataTag}
   */
  private boolean isMetadataTagged( JClassType type )
  {
    return !type.equals( d_typeMetadataTag ) && type.isAssignableTo( d_typeMetadataTag );
  }
}
